﻿using System;
using System.Collections.Generic;
using System.Configuration;
using System.Text;
using System.Globalization;
using System.Data;
using System.Linq;
using System.Text.RegularExpressions;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Security logic based on the following database views: VAdmUserInfo, VAdmUserPrivileges, VAdmBusinessUnitHierarchy, VAdmTeamMembership, VAdmOrganizationTeams, VAdmOrganizationUnits.</summary>
    /// <remarks>
    /// <para>
    /// <list type="bullet">
    /// Supported access filter options:
    /// <item><see cref="BizElements.BusinessLayer.AccessFilterOptions.PreferDistinct"/></item>
    /// <item><see cref="BizElements.BusinessLayer.AccessFilterOptions.PreferInPredicateWithSubquery"/></item>
    /// </list>
    /// </para></remarks>
    public sealed class EntitySecurityProvider : IEntitySecurityProvider
    {
        #region Configuration.

        /// <summary>Specifies whether the database accessed by BizElements is running in a single-Tenant mode, i.e. whether only one organization is using it.</summary>
        /// <value>Boolean value. Default is <b>false</b></value>
        /// <remarks>
        /// <para>Set the value to <b>true</b> if the database is in single-Tenant mode or if it doens't even support multi-org data to improve the performance 
        /// to ensure that the <b>EntitySecurityProvider</b> does not perform unnecessary checks.</para>
        /// <para>The value can be configured in the <b>appSettings</b> section of application's configuration fire:
        /// <code>
        /// &lt;add key="BizElements.BusinessLayer.EntitySecurityProvider.IsSingleTenantDatabase" value="true" /&gt;
        /// </code>
        /// </para>
        /// </remarks>
        public static readonly bool IsSingleTenantDatabase = ("true" == ConfigurationManager.AppSettings["BizElements.BusinessLayer.EntitySecurityProvider.IsSingleTenantDatabase"]);

        private static bool IsMultiTenantDatabase
        {
            get { return !IsSingleTenantDatabase; }
        }

        /// <summary>Returns <b>-1</b></summary>
        public object NonExistentId
        {
            get { return -1; }
        }

        /// <summary>Returns <see cref="BizElements.BusinessLayer.AccessFilterOptions.PreferDistinct"/>.</summary>
        public AccessFilterOptions DefaultOptions
        {
            get { return AccessFilterOptions.PreferDistinct; }
        }

        #endregion

        #region ApplyAccessFilterToQuery.

        /// <summary>Modifies query so that it returns only entities that user is allowed to access.</summary>
        /// <param name="descriptor">Composite entity descriptor.</param>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="query">Query to which join clauses and additional criteria will be added as required.</param>
        /// <param name="accessMode">Access mode: read, write or delete.</param>
        /// <param name="options">Options/hints flags.</param>        
        public void ApplyAccessFilterToQuery(IBusinessObjectDescriptor descriptor, IActor user, SelectStatement query, AccessMode accessMode, AccessFilterOptions options)
        {
            if (options == AccessFilterOptions.None)
                options = this.DefaultOptions;

            ApplyAccessFilterToQuery(descriptor, user, query.FromTable, query.Relations, query.Where, accessMode, options);
            if ((options & AccessFilterOptions.PreferDistinct) != 0)
                query.Distinct = true;
        }

        /// <summary>Modifies query so that it returns only entities that user is allowed to access.</summary>
        /// <param name="descriptor">Composite entity descriptor.</param>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="engine">Search engine to which join clauses and additional criteria will be added as required.</param>
        /// <param name="accessMode">Access mode: read, write or delete.</param>
        /// <param name="options">Options/hints flags.</param>
        public void ApplyAccessFilterToSearchEngine(IBusinessObjectDescriptor descriptor, IActor user, SearchEngine engine, AccessMode accessMode, AccessFilterOptions options)
        {
            if (options == AccessFilterOptions.None)
                options = this.DefaultOptions;

            ApplyAccessFilterToQuery(descriptor, user, engine.FromTable, engine.Relations, engine.Filter, accessMode, options);
            if ((options & AccessFilterOptions.PreferDistinct) != 0)
                engine.Distinct = true;
        }

        /// <summary>Modifies query so that it returns only entities that user is allowed to access.</summary>
        /// <param name="descriptor">Composite entity descriptor.</param>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="sourceTable">Table which contains the entities. Aliases are permitted.</param>
        /// <param name="relations">Relation bucket to which join clauses will be added as required.</param>
        /// <param name="where">Search condition to which additional criteria will be added as required.</param>
        /// <param name="accessMode">Access mode: read, write or delete.</param>
        /// <param name="options">Options/hints flags.</param>
        public void ApplyAccessFilterToQuery(IBusinessObjectDescriptor descriptor, IActor user, IDbTable sourceTable, RelationBucket relations, SearchCondition where, AccessMode accessMode, AccessFilterOptions options)
        {
            if (options == AccessFilterOptions.None)
                options = this.DefaultOptions;

            if ((options & AccessFilterOptions.PreferDistinct) != 0)
            {
                if (IsSingleTenantDatabase)
                    ApplyAccessFilterUsingLeftOuterJoinsForSingleTenantDatabase(descriptor, user, sourceTable, relations, where, accessMode);
                else
                    ApplyAccessFilterUsingLeftOuterJoinsForMultiOrgDatabase(descriptor, user, sourceTable, relations, where, accessMode);
            }
            else if ((options & AccessFilterOptions.PreferInPredicateWithSubquery) != 0)
            {
                IDbTable tbl = sourceTable.Clone(null);
                ApplyAccessFilterUsingInPredicate(descriptor, tbl, user, sourceTable, relations, where, accessMode);
            }
            else
            {
                // Default for unssupported options.
                if (IsSingleTenantDatabase)
                    ApplyAccessFilterUsingLeftOuterJoinsForSingleTenantDatabase(descriptor, user, sourceTable, relations, where, accessMode);
                else
                    ApplyAccessFilterUsingLeftOuterJoinsForMultiOrgDatabase(descriptor, user, sourceTable, relations, where, accessMode);
            }
        }

        private void ApplyAccessFilterUsingLeftOuterJoinsForMultiOrgDatabase(IBusinessObjectDescriptor meta, IActor user, IDbTable sourceTable, RelationBucket relations, SearchCondition where, AccessMode accessMode)
        {
            // Undefined assignment behavior in multiorg databases when a table doesn't have organization field.

            if (!user.HasPermission(meta.Permissions.ReadPermission))
            {
                // Exit with dummy filter if user can't access any of the existing entities.
                where.And(sourceTable.PrimaryKey[0], NonExistentId);
                return;
            }

            // In multi-Tenant tables, user can access only records that belong to their organization.
            var orgIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.OrganizationIdPropertyName);
            if (orgIdCol != null)
                where.And(orgIdCol, user.Organization);

            bool hasReadAllPermissionInMultiOrgDatabase = IsMultiTenantDatabase && user.HasPermission(meta.Permissions.ReadAllPermission);
            if (hasReadAllPermissionInMultiOrgDatabase && (orgIdCol != null))
            {
                // User can access all records that exist in the table and belong to his organization.
                // Ideal case - a table has an organization ID column. No additional security checks and filters are required.
                return;
            }
            
            SearchCondition accessFilter = new SearchCondition();
            var ownerIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.OwnerIdPropertyName);
            if (ownerIdCol != null)
            {
                if (hasReadAllPermissionInMultiOrgDatabase)
                {
                    // User cann access records that belong to all owners in his organization.
                    // This code path is used only when table doesn''t have an organization ID column.
                    VAdmUserInfoMeta owners = new VAdmUserInfoMeta();
                    DbRelation fkOwners = new DbRelation(owners, owners.UserId, sourceTable, ownerIdCol);
                    relations.Add(fkOwners, /*left outer*/ true);
                    accessFilter.Or(owners.OrganizationId, user.Organization);
                }
                else
                {
                    // User can access records that he directly owns.
                    accessFilter.Or(ownerIdCol, user.Id);
                }
            }

            IDbColumn teamIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.TeamIdPropertyName);
            if (teamIdCol != null)
            {                
                VAdmTeamMembershipMeta userTeams = new VAdmTeamMembershipMeta();
                DbRelation fkUserTeams = new DbRelation(userTeams, userTeams.TeamId, sourceTable, teamIdCol);
                relations.Add(fkUserTeams, /*left outer*/ true);

                if (hasReadAllPermissionInMultiOrgDatabase)
                {
                    // User cann access records that belong to all teams in his organization.
                    // This code path is used only when table doesn''t have an organization ID column.
                    accessFilter.Or(userTeams.OrganizationId, user.Organization);
                }
                else
                {
                    // User can access records that belong to teams he is a member of.
                    SearchCondition teamFilter = new SearchCondition(userTeams.UserId, user.Id);
                    // Teams have additional read/write/delete permissions.
                    if (accessMode == AccessMode.Write)
                        teamFilter.And(userTeams.CanWrite, true);
                    else if (accessMode == AccessMode.Delete)
                        teamFilter.And(userTeams.CanDelete, true);

                    accessFilter.Or(teamFilter);
                }
            }

            var unitIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.UnitIdPropertyName);
            if (unitIdCol != null)
            {
                if (hasReadAllPermissionInMultiOrgDatabase)
                {
                    // User cann access records that belong to all units in his organization.
                    // This code path is used only when table doesn't have an organization ID column.
                    VAdmOrganizationUnitsMeta units = new VAdmOrganizationUnitsMeta();
                    DbRelation fkUnits = new DbRelation(units, units.UnitId, sourceTable, unitIdCol);
                    relations.Add(fkUnits, /*left outer*/ true);
                    accessFilter.Or(units.OrganizationId, user.Organization);
                }
                else
                {
                    if (user.HasPermission(meta.Permissions.ReadUnitPermission))
                    {
                        // User can access all records that belong to her/his units.                    
                        accessFilter.Or(unitIdCol, user.PrimaryUnit);
                    }

                    if (user.HasPermission(meta.Permissions.ReadChildUnitPermission))
                    {
                        // User can access all records that belong to her/his child units.
                        // Organization is not checked because it is expected that all units in the hierarchy belong to the same organization.
                        VAdmBusinessUnitHierarchyMeta childUnits = new VAdmBusinessUnitHierarchyMeta();
                        DbRelation fkChildUnits = new DbRelation(childUnits, new IDbColumn[] { childUnits.ChildUnitId }, sourceTable, new IDbColumn[] { unitIdCol }, null);
                        relations.Add(fkChildUnits, /*left outer*/ true);
                        accessFilter.Or(childUnits.ParentUnitId, user.PrimaryUnit);
                    }
                }
            }

            if (user.HasPermission(meta.Permissions.ReadUnassignedPermission))
            {
                // User can access unassigned entities. 
                // All fields must be set to null except organization which has already been checked if it exists.
                // If there are non-nullable fields that means that entities cannot be unassigned.
                SearchCondition allNulls = new SearchCondition();
                if (ownerIdCol != null)
                    allNulls.And(ownerIdCol, null);

                if (unitIdCol != null)
                    allNulls.And(unitIdCol, null);

                if (teamIdCol != null)
                    allNulls.And(teamIdCol, null);

                if (!SearchCondition.IsNullOrEmpty(allNulls))
                    accessFilter.Or(allNulls);
            }

            if (!SearchCondition.IsNullOrEmpty(accessFilter))
                where.And(accessFilter);
        }

        private void ApplyAccessFilterUsingLeftOuterJoinsForSingleTenantDatabase(IBusinessObjectDescriptor meta, IActor user, IDbTable sourceTable, RelationBucket relations, SearchCondition where, AccessMode accessMode)
        {
            if (!user.HasPermission(meta.Permissions.ReadPermission))
            {
                // Exit with dummy filter if user can't access any of the existing entities.
                where.And(sourceTable.PrimaryKey[0], NonExistentId);
                return;
            }

            if (user.HasPermission(meta.Permissions.ReadAllPermission))
            {
                // User can access all records that exist in the table. No additional security checks and filters are required.
                return;
            }

            SearchCondition accessFilter = new SearchCondition();
            var ownerIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.OwnerIdPropertyName);
            if (ownerIdCol != null)
            {
                // User can access records that he directly owns.
                accessFilter.Or(ownerIdCol, user.Id);
            }

            IDbColumn teamIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.TeamIdPropertyName);
            if (teamIdCol != null)
            {
                VAdmTeamMembershipMeta userTeams = new VAdmTeamMembershipMeta();
                DbRelation fkUserTeams = new DbRelation(userTeams, userTeams.TeamId, sourceTable, teamIdCol);
                relations.Add(fkUserTeams, /*left outer*/ true);

                // User can access records that belong to teams he is a member of.
                SearchCondition teamFilter = new SearchCondition(userTeams.UserId, user.Id);
                // Teams have additional read/write/delete permissions.
                if (accessMode == AccessMode.Write)
                    teamFilter.And(userTeams.CanWrite, true);
                else if (accessMode == AccessMode.Delete)
                    teamFilter.And(userTeams.CanDelete, true);

                accessFilter.Or(teamFilter);
            }

            var unitIdCol = TryGetColumnByPropertyName(sourceTable, meta.Fields.UnitIdPropertyName);
            if (unitIdCol != null)
            {
                if (user.HasPermission(meta.Permissions.ReadUnitPermission))
                {
                    // User can access all records that belong to her/his units.                    
                    accessFilter.Or(unitIdCol, user.PrimaryUnit);
                }

                if (user.HasPermission(meta.Permissions.ReadChildUnitPermission))
                {
                    // User can access all records that belong to her/his unit and all child units.
                    // Organization is not checked because it is expected that all units in the hierarchy belong to the same organization.
                    VAdmBusinessUnitHierarchyMeta childUnits = new VAdmBusinessUnitHierarchyMeta();
                    DbRelation fkChildUnits = new DbRelation(childUnits, new IDbColumn[] { childUnits.ChildUnitId }, sourceTable, new IDbColumn[] { unitIdCol }, null);
                    relations.Add(fkChildUnits, /*left outer*/ true, false);
                    accessFilter.Or(childUnits.ParentUnitId, user.PrimaryUnit);
                }
            }

            if (user.HasPermission(meta.Permissions.ReadUnassignedPermission))
            {
                // User can access unassigned entities. 
                // All fields must be set to null except organization which has already been checked if it exists.
                // If there are non-nullable fields that means that entities cannot be unassigned.
                SearchCondition allNulls = new SearchCondition();
                if (ownerIdCol != null)
                    allNulls.And(ownerIdCol, null);

                if (unitIdCol != null)
                    allNulls.And(unitIdCol, null);

                if (teamIdCol != null)
                    allNulls.And(teamIdCol, null);

                if (!SearchCondition.IsNullOrEmpty(allNulls))
                    accessFilter.Or(allNulls);
            }

            if (!SearchCondition.IsNullOrEmpty(accessFilter))
                where.And(accessFilter);
        }

        private void ApplyAccessFilterUsingInPredicate(IBusinessObjectDescriptor meta, IDbTable tbl, IActor user, IDbTable sourceTable, RelationBucket relations, SearchCondition where, AccessMode accessMode)
        {
            SelectStatement subquery;
            if (IsSingleTenantDatabase)
                subquery = BuildSelectEntityIDsForUserInSingleTenantDatabase(meta, tbl, user, accessMode);
            else
                subquery = BuildSelectEntityIDsForUserInMultiOrgDatabase(meta, tbl, user, accessMode);

            if (!SearchCondition.IsNullOrEmpty(subquery.Where))
                where.And(PredicateFactory.In(sourceTable.PrimaryKey[0], subquery));
        }

        #endregion

        #region BuildSelectEntityIDsForUserInSingleTenantDatabase*.

        ///// <summary>Builds <see cref="BizElements.Core.SelectStatement"/> that fetches IDs of all entities that can be access by given actor.</summary>
        ///// <param name="user">Actor for whom row access permissions are to be checked.</param>
        ///// <param name="accessMode">Access mode: read, write or delete.</param>
        ///// <returns>Select statement with primary key. You may add more columns and modify statement.</returns>
        private SelectStatement BuildSelectEntityIDsForUserInSingleTenantDatabase(IBusinessObjectDescriptor meta, IDbTable tbl, IActor user, AccessMode accessMode)
        {  
            if (tbl.PrimaryKey.Count != 1)
                throw new InvalidOperationException("Only tables with single field PK are supported. Table name: " + tbl.TableName);

            SelectStatement select = new SelectStatement(tbl, tbl.PrimaryKey[0]);
            if (!user.HasPermission(meta.Permissions.ReadPermission))
            {
                // Exit with dummy filter if user can't access any of the existing entities.
                select.Where.And(tbl.PrimaryKey[0], NonExistentId);
                return select;
            }

            if (user.HasPermission(meta.Permissions.ReadAllPermission))
            {
                // User can access all records that exist in the table and belong to his organization. No additional security checks and filters are required.
                return select;
            }

            var ownerIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.OwnerIdPropertyName);
            if (ownerIdCol != null)
            {
                // User can access records that he directly owns.
                select.Where.Or(ownerIdCol, user.Id);
            }

            IDbColumn teamIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.TeamIdPropertyName);
            if (teamIdCol != null)
            {
                // User can access records that belong to teams he is a member of.
                VAdmTeamMembershipMeta userTeams = new VAdmTeamMembershipMeta();
                DbRelation fkUserTeams = new DbRelation(userTeams, new IDbColumn[] { userTeams.TeamId }, tbl, new IDbColumn[] { teamIdCol }, null);
                select.Relations.Add(fkUserTeams, /*left outer*/ true, false);

                // Teams have additional read/write/delete permissions.
                SearchCondition teamFilter = new SearchCondition(userTeams.UserId, user.Id);
                if (accessMode == AccessMode.Write)
                    teamFilter.And(userTeams.CanWrite, true);
                else if (accessMode == AccessMode.Delete)
                    teamFilter.And(userTeams.CanDelete, true);

                select.Where.Or(teamFilter);
            }

            var unitIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.UnitIdPropertyName);
            if (unitIdCol != null)
            {
                if (user.HasPermission(meta.Permissions.ReadUnitPermission))
                {
                    // User can access all records that belong to her/his units.
                    select.Where.Or(unitIdCol, user.PrimaryUnit);
                }

                if (user.HasPermission(meta.Permissions.ReadChildUnitPermission))
                {
                    // User can access all records that belong to her/his unit and all child units.
                    VAdmBusinessUnitHierarchyMeta childUnits = new VAdmBusinessUnitHierarchyMeta();
                    DbRelation fkChildUnits = new DbRelation(childUnits, new IDbColumn[] { childUnits.ChildUnitId }, tbl, new IDbColumn[] { unitIdCol }, null);
                    select.Relations.Add(fkChildUnits, /*left outer*/ true, false);
                    select.Where.Or(childUnits.ParentUnitId, user.PrimaryUnit);
                }
            }

            if (user.HasPermission(meta.Permissions.ReadUnassignedPermission))
            {
                SearchCondition allNulls = new SearchCondition();
                if (ownerIdCol != null)
                    allNulls.And(ownerIdCol, null);

                if (unitIdCol != null)
                    allNulls.And(unitIdCol, null);

                if (teamIdCol != null)
                    allNulls.And(teamIdCol, null);

                if (!SearchCondition.IsNullOrEmpty(allNulls))
                    select.Where.Or(allNulls);
            }

            return select;
        }

        ///// <summary>Builds <see cref="BizElements.Core.SelectStatement"/> that fetches IDs of all entities that can be access by given actor.</summary>
        ///// <param name="user">Actor for whom row access permissions are to be checked.</param>
        ///// <param name="accessMode">Access mode: read, write or delete.</param>
        ///// <returns>Select statement with primary key. You may add more columns and modify statement.</returns>
        private SelectStatement BuildSelectEntityIDsForUserInMultiOrgDatabase(IBusinessObjectDescriptor meta, IDbTable tbl, IActor user, AccessMode accessMode)
        {
            if (tbl.PrimaryKey.Count != 1)
                throw new InvalidOperationException("Only tables with single field PK are supported. Table name: " + tbl.TableName);

            SelectStatement select = new SelectStatement(tbl, tbl.PrimaryKey[0]);
            if (!user.HasPermission(meta.Permissions.ReadPermission))
            {
                // Exit with dummy filter if user can't access any of the existing entities.
                select.Where.And(tbl.PrimaryKey[0], NonExistentId);
                return select;
            }

            // In multi-Tenant tables, user can access only records that belong to their organization.
            var orgIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.OrganizationIdPropertyName);
            if (orgIdCol != null)
                select.Where.And(orgIdCol, user.Organization);

            bool hasReadAllPermissionInMultiOrgDatabase = IsMultiTenantDatabase && user.HasPermission(meta.Permissions.ReadAllPermission);
            if (hasReadAllPermissionInMultiOrgDatabase && (orgIdCol != null))
            {
                // User can access all records that exist in the table and belong to his organization.
                // Ideal case - a table has an organization ID column. No additional security checks and filters are required.
                return select;
            }

            SearchCondition accessFilter = new SearchCondition();
            var ownerIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.OwnerIdPropertyName);
            if (ownerIdCol != null)
            {
                if (hasReadAllPermissionInMultiOrgDatabase)
                {
                    // User cann access records that belong to all owners in his organization.
                    // This code path is used only when table doesn''t have an organization ID column.
                    VAdmUserInfoMeta owners = new VAdmUserInfoMeta();
                    DbRelation fkOwners = new DbRelation(owners, owners.UserId, tbl, ownerIdCol);
                    select.Relations.Add(fkOwners, /*left outer*/ true);
                    accessFilter.Or(owners.OrganizationId, user.Organization);
                }
                else
                {
                    // User can access records that he directly owns.
                    accessFilter.Or(ownerIdCol, user.Id);
                }
            }

            IDbColumn teamIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.TeamIdPropertyName);
            if (teamIdCol != null)
            {
                // User can access records that belong to teams he is a member of.
                VAdmTeamMembershipMeta userTeams = new VAdmTeamMembershipMeta();
                DbRelation fkUserTeams = new DbRelation(userTeams, new IDbColumn[] { userTeams.TeamId }, tbl, new IDbColumn[] { teamIdCol }, null);
                select.Relations.Add(fkUserTeams, /*left outer*/ true);

                if (hasReadAllPermissionInMultiOrgDatabase)
                {
                    // User cann access records that belong to all teams in his organization.
                    // This code path is used only when table doesn''t have an organization ID column.
                    accessFilter.Or(userTeams.OrganizationId, user.Organization);
                }
                else
                {
                    // User can access records that belong to teams he is a member of.                    
                    SearchCondition teamFilter = new SearchCondition(userTeams.UserId, user.Id);
                    // Teams have additional read/write/delete permissions.
                    if (accessMode == AccessMode.Write)
                        teamFilter.And(userTeams.CanWrite, true);
                    else if (accessMode == AccessMode.Delete)
                        teamFilter.And(userTeams.CanDelete, true);

                    accessFilter.Or(teamFilter);
                }
            }

            var unitIdCol = TryGetColumnByPropertyName(tbl, meta.Fields.UnitIdPropertyName);
            if (unitIdCol != null)
            {
                if (hasReadAllPermissionInMultiOrgDatabase)
                {
                    // User cann access records that belong to all units in his organization.
                    // This code path is used only when table doesn't have an organization ID column.
                    VAdmOrganizationUnitsMeta units = new VAdmOrganizationUnitsMeta();
                    DbRelation fkUnits = new DbRelation(units, units.UnitId, tbl, unitIdCol);
                    select.Relations.Add(fkUnits, /*left outer*/ true);
                    accessFilter.Or(units.OrganizationId, user.Organization);
                }
                else
                {
                    if (user.HasPermission(meta.Permissions.ReadUnitPermission))
                    {
                        // User can access all records that belong to her/his units.
                        accessFilter.Or(unitIdCol, user.PrimaryUnit);
                    }

                    if (user.HasPermission(meta.Permissions.ReadChildUnitPermission))
                    {
                        // User can access all records that belong to her/his unit and all child units.
                        VAdmBusinessUnitHierarchyMeta childUnits = new VAdmBusinessUnitHierarchyMeta();
                        DbRelation fkChildUnits = new DbRelation(childUnits, new IDbColumn[] { childUnits.ChildUnitId }, tbl, new IDbColumn[] { unitIdCol }, null);
                        select.Relations.Add(fkChildUnits, /*left outer*/ true);
                        accessFilter.Or(childUnits.ParentUnitId, user.PrimaryUnit);
                    }
                }
            }

            if (user.HasPermission(meta.Permissions.ReadUnassignedPermission))
            {
                // User can access unassigned entities. 
                // All fields must be set to null except organization which has already been checked if it exists.
                // If there are non-nullable fields that means that entities cannot be unassigned.
                SearchCondition allNulls = new SearchCondition();
                if (ownerIdCol != null)
                    allNulls.And(ownerIdCol, null);

                if (unitIdCol != null)
                    allNulls.And(unitIdCol, null);

                if (teamIdCol != null)
                    allNulls.And(teamIdCol, null);

                if (!SearchCondition.IsNullOrEmpty(allNulls))
                    accessFilter.Or(allNulls);
            }

            if (!SearchCondition.IsNullOrEmpty(accessFilter))
                select.Where.And(accessFilter);

            return select;
        }

        #endregion

        #region CanReadEntity.

        /// <summary>Check whether the user can open/read the specified entity. <b>RPC enabled.</b></summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to access. Primary key must be set.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public bool CanReadEntity(IActor user, IBusinessObject entity)
        {
            IBusinessObjectDescriptor meta = entity.GetDescriptor();
            if (user.HasPermission(meta.Permissions.ReadAllPermission))
                return true;

            bool canRead = RpcHelper.InvokeMethod<bool, IActor, IBusinessObject, AccessMode>(this, DbCanAccessEntity, user, entity, AccessMode.Read);
            return canRead;
        }

        #endregion

        #region CanSaveEntity.

        /// <summary>Checks whether the user can acces and save the specified entity. <b>RPC enabled.</b></summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to save.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public bool CanSaveEntity(IActor user, IBusinessObject entity)
            
        {
            if (entity.IsNew)
                return CanCreateEntity(user, entity);
            else
                return CanUpdateEntity(user, entity);
        }

        #endregion

        #region CanCreateEntity.

        /// <summary>Checks whether the user can insert the provided entity. <b>RPC enabled.</b></summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to insert into database.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public bool CanCreateEntity(IActor user, IBusinessObject entity)            
        {
            if (!entity.IsNew)
                throw new ArgumentException("Entity must be new.");

            IBusinessObjectDescriptor meta = entity.GetDescriptor();
            if (!user.HasPermission(meta.Permissions.CreatePermission))
                return false;

            bool canAssign = CanAssignOwnership(user, entity, meta);
            return canAssign;
        }

        #endregion

        #region CanUpdateEntity.

        /// <summary>Checks whether the user can update the provided entity. <b>RPC enabled.</b></summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to update.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public bool CanUpdateEntity(IActor user, IBusinessObject entity)            
        {
            if (entity.IsNew)
                throw new ArgumentException("Entity must already exist.");

            IBusinessObjectDescriptor meta = entity.GetDescriptor();
            if (!user.HasPermission(meta.Permissions.UpdatePermission))
                return false;

            // First we check if actor is attempting to reassign ownership.
            IDbColumn ownerIdCol, unitIdCol, teamIdCol, organizationIdCol;
            object newOwnerId, newUnitId, newTeamId, newOrganizationId;
            GetOwnershipFieldsAndValue(entity, out ownerIdCol, out newOwnerId, out unitIdCol, out newUnitId, out teamIdCol, out newTeamId, out organizationIdCol, out newOrganizationId);
            
            // User's and entity's organizations must be equal.
            if (organizationIdCol != null)
            {
                if (!EqualValues(user.Organization, newOrganizationId))
                    return false;
            }

            bool hasOwnershipFields = (ownerIdCol != null) || (unitIdCol != null) || (teamIdCol != null) || (organizationIdCol != null);
            if (hasOwnershipFields)
            {
                IEntity oldVersion = entity.GetSavedVersion();
                if (oldVersion == null)
                    throw new InvalidOperationException("Cannot retrieve old version.");

                object oldOwnerId = (ownerIdCol != null) ? oldVersion.GetField(ownerIdCol) : null;
                object oldUnitId = (unitIdCol != null) ? oldVersion.GetField(unitIdCol) : null;
                object oldTeamId = (teamIdCol != null) ? oldVersion.GetField(teamIdCol) : null;
                object oldOrganizationId = (organizationIdCol != null) ? oldVersion.GetField(organizationIdCol) : null;

                // Can't change organization.
                bool isTryingToChangeOrg = !EqualValues(newOrganizationId, oldOrganizationId);
                if (isTryingToChangeOrg)
                    return false;

                bool isReassigning = !EqualValues(newOwnerId, oldOwnerId) || !EqualValues(newUnitId, oldUnitId) || !EqualValues(newTeamId, oldTeamId);
                if (isReassigning)
                {
                    bool canAssign = CanAssignOwnership(user, entity, meta);
                    if (!canAssign)
                        return false;
                }
            }

            bool canAccessAnyEntity = user.HasPermission(meta.Permissions.ReadAllPermission);
            if (canAccessAnyEntity)
                return true;

            bool canWrite = RpcHelper.InvokeMethod<bool, IActor, IBusinessObject, AccessMode>(this, DbCanAccessEntity, user, entity, AccessMode.Write);
            return canWrite;
        }

        #endregion

        #region CanDeleteEntity.

        /// <summary>Check whether the user can acces and delete/deactivate the specified entity. <b>RPC enabled.</b></summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to delete.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public bool CanDeleteEntity(IActor user, IBusinessObject entity)            
        {
            IBusinessObjectDescriptor meta = entity.GetDescriptor();
            if (!user.HasPermission(meta.Permissions.DeletePermission))
                return false;

            bool canAccessAnyEntity = user.HasPermission(meta.Permissions.ReadAllPermission);
            if (canAccessAnyEntity)
                return true;

            bool canDelete = RpcHelper.InvokeMethod<bool, IActor, IBusinessObject, AccessMode>(this, DbCanAccessEntity, user, entity, AccessMode.Delete);
            return canDelete;
        }

        #endregion

        #region CanAssignEntity.

        /// <summary>Checks whether the user can (re)assign ownership of the provided entity. <b>RPC enabled.</b></summary>
        /// <param name="user">Actor for whom row access permissions are to be checked.</param>
        /// <param name="entity">Entity which the user is trying to insert into database.</param>
        /// <returns><b>true</b> if user has privileges to access the entity; <b>false</b> otherwise.</returns>
        public bool CanAssignEntity(IActor user, IBusinessObject entity)            
        {
            IBusinessObjectDescriptor meta = entity.GetDescriptor();
            return CanAssignOwnership(user, entity, meta);
        }

        private static bool CanAssignOwnership(IActor user, IBusinessObject entity, IBusinessObjectDescriptor meta)             
        {
            IDbColumn ownerIdCol, unitIdCol, teamIdCol, organizationIdCol;
            object ownerId, unitId, teamId, organizationId;
            GetOwnershipFieldsAndValue(entity, out ownerIdCol, out ownerId, out unitIdCol, out unitId, out teamIdCol, out teamId, out organizationIdCol, out organizationId);

            // User's and entity's organizations must be equal.
            if (organizationIdCol != null)
            {
                if (!EqualValues(user.Organization, organizationId))
                    return false;
            }

            if ((unitIdCol != null) && (unitId != null))
            {
                if (!CanAssignToUnit(user, meta, unitId))
                    return false;
            }

            if ((teamIdCol != null) && (teamId != null))
            {
                if (!CanAssignToTeam(user, meta, teamId))
                    return false;
            }

            if ((ownerIdCol != null) && (ownerId != null))
            {
                if (!CanAssignOwner(user, meta, ownerId, unitId, teamId))
                    return false;
            }

            return true;
        }

        private static bool CanAssignToUnit(IActor user, IBusinessObjectDescriptor meta, object unitId)
        {
            // Must belong to same organizationa actor.
            // User can assign to his own unit.
            // User can assign to child units if he/she has required permission.
            // Must have special permission to assign to other units in his organization.

            if (IsMultiTenantDatabase)
            {
                bool belongsToUserOrganization = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsUnitFromUserOrganization, user.Organization, unitId);
                if (!belongsToUserOrganization)
                    return false;
            }

            if (user.HasPermission(meta.Permissions.AssignAnyUnitPermission))
                return true;

            if (EqualValues(user.PrimaryUnit, unitId))
                return true;

            if (user.HasPermission(meta.Permissions.AssignChildUnitPermission))
            {
                bool isChildUnit = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsChildUnit, user.PrimaryUnit, unitId);
                if (isChildUnit)
                    return true;
            }

            return false;
        }        

        private static bool DbIsUnitFromUserOrganization(object actorOrganizationId, object unitId)
        {
            VAdmOrganizationUnitsMeta units = new VAdmOrganizationUnitsMeta();
            SelectStatement selectPair = new SelectStatement(units, units.UnitId, units.OrganizationId);
            selectPair.Where.And(units.UnitId, unitId);
            selectPair.Where.And(units.OrganizationId, actorOrganizationId);

            var data = selectPair.Execute();
            bool isUnitInOrg = (data.Rows.Count > 0);
            return isUnitInOrg;
        }

        private static bool DbIsChildUnit(object parentUnitId, object childUnitId)
        {
            VAdmBusinessUnitHierarchyMeta units = new VAdmBusinessUnitHierarchyMeta();
            SelectStatement selectPair = new SelectStatement(units, units.ParentUnitId, units.ChildUnitId);
            selectPair.Where.And(units.ParentUnitId, parentUnitId);
            selectPair.Where.And(units.ChildUnitId, childUnitId);

            var data = selectPair.Execute();
            bool existsRelation = (data.Rows.Count > 0);
            return existsRelation;
        }

        private static bool DbIsInUnit(object unitId, object userId)
        {
            VAdmUserInfoMeta users = new VAdmUserInfoMeta();
            SelectStatement select = new SelectStatement(users, users.UserId, users.BusinessUnitId);
            select.Where.And(users.UserId, userId);
            select.Where.And(users.BusinessUnitId, unitId);

            var data = select.Execute();
            bool isInUnit = (data.Rows.Count > 0);
            return isInUnit;
        }

        private static bool CanAssignToTeam(IActor user, IBusinessObjectDescriptor meta, object teamId)
        {
            // User can assing to one his teams.
            // Must have special permission to assign to other teams that belong to his organization.

            if (IsMultiTenantDatabase)
            {
                bool belongsToUserOrganization = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsTeamFromUserOrganization, user.Organization, teamId);
                if (!belongsToUserOrganization)
                    return false;
            }

            if (user.HasPermission(meta.Permissions.AssignAnyTeamPermission))
                return true;

            bool isTeamMember = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsTeamMemberAndCanWrite, teamId, user.Id);
            return isTeamMember;
        }

        private static bool DbIsTeamFromUserOrganization(object actorOrganizationId, object teamId)
        {
            VAdmOrganizationTeamsMeta teams = new VAdmOrganizationTeamsMeta();
            SelectStatement selectPair = new SelectStatement(teams, teams.OrganizationId, teams.TeamId);
            selectPair.Where.And(teams.TeamId, teamId);
            selectPair.Where.And(teams.OrganizationId, actorOrganizationId);

            var data = selectPair.Execute();
            bool isTeamInOrg = (data.Rows.Count > 0);
            return isTeamInOrg;
        }

        private static bool DbIsTeamMemberAndCanWrite(object teamId, object userId)
        {
            VAdmTeamMembershipMeta userTeams = new VAdmTeamMembershipMeta();
            SelectStatement select = new SelectStatement(userTeams, userTeams.TeamId, userTeams.UserId);
            select.Where.And(userTeams.UserId, userId);
            select.Where.And(userTeams.TeamId, teamId);
            select.Where.And(userTeams.CanWrite, true);

            var data = select.Execute();
            bool isMember = (data.Rows.Count > 0);
            return isMember;
        }

        private static bool CanAssignOwner(IActor user, IBusinessObjectDescriptor meta, object ownerId, object unitId, object teamId)
        {            
            // Unit and team assignment permissions are checked via CanAssignUnit() and CanAssignTeam() methods.

            // If a unit is assigned then the owner must belong to that unit.
            if (unitId != null)
            {                
                bool isOwnerInUnit = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsInUnit, unitId, ownerId);
                if (!isOwnerInUnit)
                    return false;
            }

            // If a team is assigned then the owner must belong to that team.
            if (teamId != null)
            {                
                bool isOwnerMemberOfTeam = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsTeamMember, teamId, ownerId);
                if (!isOwnerMemberOfTeam)
                    return false;
            }

            // Actor can assign to himself.
            if (EqualValues(user.Id, ownerId))
                return true;

            // Owner must belong to the same organization as actor.
            if (!IsSingleTenantDatabase)
            {
                bool belongsToSameOrganization = RpcHelper.InvokeStaticMethod<bool, object, object>(DbIsOwnerFromUserOrganization, user.Organization, ownerId);
                if (!belongsToSameOrganization)
                    return false;
            }

            // Actor can assign to other users that belong to his organization ih he has special permission.
            if (user.HasPermission(meta.Permissions.AssignOwnerPermission))
                return true;

            return false;
        }

        private static bool DbIsTeamMember(object teamId, object userId)
        {
            VAdmTeamMembershipMeta userTeams = new VAdmTeamMembershipMeta();
            SelectStatement select = new SelectStatement(userTeams, userTeams.TeamId, userTeams.UserId);
            select.Where.And(userTeams.UserId, userId);
            select.Where.And(userTeams.TeamId, teamId);

            var data = select.Execute();
            bool isMember = (data.Rows.Count > 0);
            return isMember;
        }

        private static bool DbIsOwnerFromUserOrganization(object actorOrganizationId, object ownerId)
        {
            VAdmUserInfoMeta users = new VAdmUserInfoMeta();
            SelectStatement selectPair = new SelectStatement(users, users.OrganizationId, users.UserId);
            selectPair.Where.And(users.UserId, ownerId);
            selectPair.Where.And(users.OrganizationId, actorOrganizationId);

            var data = selectPair.Execute();
            bool isOwnerInOrg = (data.Rows.Count > 0);
            return isOwnerInOrg;
        }

        #endregion

        #region DbCanAccessEntity - private.

        private bool DbCanAccessEntity(IActor user, IBusinessObject entity, AccessMode accessMode)            
        {
            IDbTable tbl = entity.Table;
            SelectStatement selectOne = new SelectStatement(tbl, tbl.PrimaryKey);
            selectOne.Top = 1;

            ApplyAccessFilterToQuery(entity.GetDescriptor(), user, selectOne, accessMode, AccessFilterOptions.None);
            if (SearchCondition.IsNullOrEmpty(selectOne.Where))
                return true;

            object[] pkValues = entity.GetPrimaryKeyValue();
            for (int i = 0; i < tbl.PrimaryKey.Count; i++)
                selectOne.Where.And(tbl.PrimaryKey[i], pkValues[i]);

            var data = selectOne.Execute();
            bool canRead = (data.Rows.Count > 0);
            return canRead;
        }

        #endregion

        #region FetchActorInfo.

        /// <summary>Fetches actor info associated with the provided username. <b>Requires direct DB access. Doesn't support RPC</b>.</summary>
        /// <param name="username">Username.</param>
        /// <returns><see cref="IActor"/> or <b>null</b> it the specified username cannot be found.</returns>
        public IActor FetchActorInfo(string username)
        {
            VAdmUserInfoEntity criteria = new VAdmUserInfoEntity() { Username = username };
            DataTable userData = new EntityDAO(criteria).Find(1);
            if (userData.Rows.Count < 1)
                return null;

            VAdmUserInfoEntity userInfo = (VAdmUserInfoEntity)userData.Rows[0];
            List<VAdmUserPrivilegesEntity> userPrivileges = FetchUserPrivileges(userInfo);
            Actor actor = CreateActorInfo(userInfo, userPrivileges);
            return actor;
        }

        private static List<VAdmUserPrivilegesEntity> FetchUserPrivileges(VAdmUserInfoEntity userInfo)
        {
            VAdmUserPrivilegesMeta privileges = new VAdmUserPrivilegesMeta();
            SelectStatement selectFunctionIDs = new SelectStatement(privileges, privileges.Columns);
            selectFunctionIDs.Where.And(privileges.UserId, userInfo.UserId);
            List<VAdmUserPrivilegesEntity> userPrivileges = new List<VAdmUserPrivilegesEntity>();
            selectFunctionIDs.FillCollection(userPrivileges, privileges.NewEntityReader());

            return userPrivileges;
        }

        private static Actor CreateActorInfo(VAdmUserInfoEntity userInfo, List<VAdmUserPrivilegesEntity> userPrivileges)
        {
            Actor actor = new Actor(userInfo.Username, userInfo.Username) { PrimaryUnit = userInfo.BusinessUnitId };
            List<int> functionIDs = ArrayUtil.Convert<List<int>, VAdmUserPrivilegesEntity, int>(userPrivileges, (entity) => entity.FunctionId);
            actor.SetPermissionsForFunctions(functionIDs);

            Dictionary<string, string> extProperties = new Dictionary<string, string>();
            extProperties["FullName"] = userInfo.FullName;
            extProperties["BusinessUnitName"] = userInfo.BusinessUnitName;
            extProperties["LanguageCode"] = userInfo.LanguageCode;
            extProperties["CultureCode"] = userInfo.CultureCode;
            actor.ExtendedProperties = new PropertyCollection<string>(true, extProperties);

            return actor;
        }

        #endregion

        #region Util.

        private static IDbColumn TryGetColumnByPropertyName(IDbTable table, string propertyName)
        {
            if (string.IsNullOrEmpty(propertyName))
                return null;

            return table.Columns.GetByPropertyName(propertyName);
        }

        private static bool EqualValues(object x, object y)
        {
            return Convert.ToString(x, CultureInfo.InvariantCulture) == Convert.ToString(y, CultureInfo.InvariantCulture);
        }

        private static void GetOwnershipFieldsAndValue(IBusinessObject entity, out IDbColumn ownerIdCol, out object ownerId, out IDbColumn unitIdCol, out object unitId, out IDbColumn teamIdCol, out object teamId, out IDbColumn organizationIdCol, out object organizationId)
        {
            IBusinessObjectDescriptor meta = entity.GetDescriptor();

            ownerIdCol = TryGetColumnByPropertyName(entity.Table, meta.Fields.OwnerIdPropertyName);
            ownerId = (ownerIdCol != null) ? entity.GetField(ownerIdCol) : null;

            unitIdCol = TryGetColumnByPropertyName(entity.Table, meta.Fields.UnitIdPropertyName);
            unitId = (unitIdCol != null) ? entity.GetField(unitIdCol) : null;

            teamIdCol = TryGetColumnByPropertyName(entity.Table, meta.Fields.TeamIdPropertyName);
            teamId = (teamIdCol != null) ? entity.GetField(teamIdCol) : null;

            organizationIdCol = TryGetColumnByPropertyName(entity.Table, meta.Fields.OrganizationIdPropertyName);
            organizationId = (organizationIdCol != null) ? entity.GetField(organizationIdCol) : null;
        }

        #endregion

        #region ImplementsRecordOwnership.

        /// <summary>Returns <b>true</b> if entity implement record ownership, i.e. if it has organization ID or owner ID or team ID or unit ID field.</summary>
        /// <param name="entity">Business object.</param>
        /// <returns><b>true</b> if entity has at least one ownership field defined in its descriptor; otherwise <b>false</b></returns>
        public bool ImplementsRecordOwnership(IBusinessObject entity)
        {
            var meta = entity.GetDescriptor();
            bool hasOwnerOrTeamOrUnitField = !string.IsNullOrEmpty(meta.Fields.OrganizationIdPropertyName) || !string.IsNullOrEmpty(meta.Fields.OwnerIdPropertyName) || !string.IsNullOrEmpty(meta.Fields.TeamIdPropertyName) || !string.IsNullOrEmpty(meta.Fields.UnitIdPropertyName);
            return hasOwnerOrTeamOrUnitField;
        }

        #endregion
    }
}
